// 解析vue
import fp from "fastify-plugin";
import { FastifyInstance, FastifyRequest } from "fastify";
import chalk from "chalk";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import {
  parse,
  SFCDescriptor,
  compileTemplate,
  compileStyleAsync,
} from "@vue/component-compiler-utils";
import { ServerConfig } from "../../config";
import { resolveCompiler } from "../../utils";

const fsPromises = fs.promises;

const cachedMap = new Map<string, SFCDescriptor>();

interface VueFastifyQuerystring {
  type: string;
  index: number; // css type
}

export default fp(async (instance: FastifyInstance, options: ServerConfig) => {
  const compiler = resolveCompiler(options.root);
  instance.addHook(
    "preHandler",
    async (
      request: FastifyRequest<{
        Querystring: VueFastifyQuerystring;
      }>,
      reply
    ) => {
      const url = request.url.split("?")[0];
      if (url.split("?")[0].endsWith(".vue")) {
        let descriptor = await genDescriptor(url);
        // if genDescriptor cannot handle all thre error
        if (!descriptor) {
          throw new Error(`[tserve error]:
                        generate descriptor error on resolve vue file.file path:${url}.`);
        }
        let scopeId;
        if (descriptor.styles.some((s) => s.scoped)) {
          // generate hash
          scopeId =
            "data-v-" +
            crypto.createHash("sha1").update(url).digest("hex").substring(0, 6);
        }

        if (request.query.type) {
          // if request is type of template
          if (request.query.type === "template" && descriptor.template) {
            try {
              const compileTemplateResult = compileTemplate({
                source: descriptor.template.content,
                filename: url,
                compiler,
              });
              reply.header("Content-Type", "application/javascript");
              return reply.send(
                `${compileTemplateResult.code}\n export { render, staticRenderFns }`
              );
            } catch (e) {
              /* handle error */
              throw new Error(e);
            }
          }
          if (request.query.type === "style" && descriptor.styles) {
            try {
              // TODO: fix compile style
              const compileStyleResult = await compileStyleAsync({
                source: descriptor.styles[request.query.index].content,
                filename: url,
                id: scopeId ? scopeId : url,
                scoped: scopeId !== undefined,
                // for scss sass
                preprocessLang: descriptor.styles[request.query.index].lang,
              });
              reply.type("text/css");
              return reply.send(compileStyleResult.code);
            } catch (e) {
              /* handle error */
              throw new Error(e);
            }
          }
        }
        let code = `import { vueHotLoad } from '/tserve/vue.js'\n`;
        if (descriptor.template) {
          code += `import { render as _render, staticRenderFns as _staticRenderFns }
          from '${url}?type=template&t=${Date.now()}'\n`;
        }
        if (descriptor.script) {
          const codeContent = await instance.rewriteImport(
            descriptor.script.content,
            url.substring(1)
          );
          code += codeContent.replace(/export\s+default/, "const __script = ");

          if (descriptor.template) {
            code += `\n__script.render = _render
                   \n__script.staticRenderFns = _staticRenderFns`;
          }
          if (scopeId) {
            code += `\n__script._scopeId='${scopeId}'`;
          }
          // for hot module reload
          code += `\nvueHotLoad('${scopeId?.split("-")[2]}', __script) `;
          code += `\n export default __script`;
        }
        // if has style
        if (descriptor.styles) {
          for (const coon of instance.esConnections) {
            coon.write(
              `event:css-update\ndata:{"id":"${url}","length":${descriptor.styles.length}}\n\n`
            );
          }
        }

        reply.header("Content-Type", "application/javascript");
        return reply.send(code);
      }
    }
  );

  // fot hot reload
  instance.get("/tserve/vue.js", async (_, reply) => {
    // console.debug(__dirname, process.cwd(), path.dirname('node_modules/tserve'))
    // FIXME:because in development evvironment thie tserve module is symbol link for
    // test package, this __dirname is not the node_modules's relative path of tserve,
    // so we need to rebuild it.
    return reply.sendFile(
      path.join(
        "node_modules",
        __dirname.substring(__dirname.indexOf("/tserve") + 1),
        "client.js"
      )
    );
  });

  // TODO: handle file change
  // watch file
  instance.watcher.on("change", async (path) => {
    if (path.endsWith("vue")) {
      path = `/${path}`;
      cachedMap.delete(path);
      console.log(`File changed on path: ${path}`);
      const descriptor = await genDescriptor(path);
      if (!descriptor) {
        console.error(`[tserve error]:
                      generate descriptor error on resolve vue file.path: ${path}`);
        return;
      }
      const data = {
        id: path,
        _scopeId: crypto
          .createHash("sha1")
          .update(path)
          .digest("hex")
          .substring(0, 6),
      };
      instance.esConnections.forEach((coon) => {
        const sendmsg = `event:module-update\ndata:${JSON.stringify(data)}\n\n`;
        console.debug(`sendmsg:${sendmsg}`);
        coon.write(sendmsg);
      });
    }
  });

  // generate sfc descriptor
  async function genDescriptor(
    url: string
  ): Promise<SFCDescriptor | undefined> {
    // if cached
    if (cachedMap.has(url)) {
      return cachedMap.get(url);
    }
    console.debug(`loading vue file:${url}`);
    // TODO: parse vue
    const root = options.root;
    const filePath = path.resolve(root, url.substring(1));
    let fileContent = "";
    try {
      fileContent = await fsPromises.readFile(filePath, {
        encoding: "utf-8",
        flag: "r",
      });
    } catch (e) {
      /* handle error */
      console.error(e, chalk.red(`file path ${filePath}`));
    }
    // compile
    try {
      const descriptor = parse({
        source: fileContent,
        filename: filePath,
        compiler,
        needMap: true,
        sourceRoot: options.root,
      });
      cachedMap.set(url, descriptor);
      return descriptor;
    } catch (e) {
      /* handle error */
      console.error(chalk.red(e), `fileContent:${fileContent}`);

      throw new Error(`cannot parse file: ${fileContent}`);
    }
  }
});
